<?php
/**
 * I know no such things as genius,it is nothing but labor and diligence.
 * @copyright (c) 2015-2016 LXSEA All rights reserved.
 * @license http://www.apache.org/licenses/LICENSE-2.0
 * @author <657306123@qq.com> LXSEA
 */

namespace Common\Util;

use Think\Exception;
use Think\Log;

final class WechatMinappUtil {
    /**
     * 企业付款 提现到零钱
     *
     * @param string $token
     * @param string $openId
     * @param string $tradeNo
     * @param float $amount
     * @param string $remark
     * @param string $trueName
     * @return array
     * @throws Exception
     */
    public static function transfers($openId, $tradeNo, $amount, $remark){
        $paymentInfo = self::getPaymentSetInfo();
        //基本参数
        $parameters = array();
        $parameters['mch_appid'] = $paymentInfo['wxappid'];
        $parameters['mchid'] = $paymentInfo['wxmchid'];
        $parameters['nonce_str'] = self::buildNonceStr();
        $parameters['partner_trade_no'] = trim($tradeNo);
        $parameters['openid'] = $openId;
        $parameters['check_name'] = 'NO_CHECK';
        $parameters['amount'] = $amount * 100;
        $parameters['desc'] = $remark;
        $parameters['spbill_create_ip'] = get_client_ip(0);

        if(empty($parameters['partner_trade_no'])) throw new Exception('订单号不能为空！', 401);
        if(empty($parameters['openid'])) throw new Exception('openid不能为空！', 401);
        if($parameters['amount'] <= 0) throw new Exception('金额不合法！', 401);
        if(empty($parameters['desc'])) throw new Exception('描述信息不能为空！', 401);

        //检测证书安全性
        $cert = self::certHandler();
        $cert = array('SSLCERT' => $cert[0], 'SSLKEY' => $cert[1],);
        //参数排序
        ksort($parameters, SORT_STRING);
        //生成签名
        $parameters['sign'] = self::makeSign($parameters, $paymentInfo['wxpaysignkey']);
        //编译请求参数为xml进行请求
        $xml = self::buildParameterToXml($parameters);
        $url = 'https://api.mch.weixin.qq.com/mmpaymkttransfers/promotion/transfers';
        $result = self::postXmlCurl($xml, $url, $cert);
        $result = self::parseXml($result);

        //检查微信返回接口是否错误
        if($result['return_code'] != 'SUCCESS') throw new Exception("付款失败[{$result['return_msg']}]", 50001);
        if($result['result_code'] != 'SUCCESS') throw new Exception("付款失败({$result['err_code']})[{$result['err_code_des']}]", 50002);
        return $result;
    }

    /**
     * 统一下单
     *
     * @param string $token
     * @param string $openId
     * @param array $parameters
     * @return array
     * @throws Exception
     */
    public static function unifiedOrder($openId, $parameters = array()){
        $BasePath=SITE_PATH."/Application/Common";
        require_once($BasePath.'/Weixinpay/WxPayData.class.php');
        require_once($BasePath.'/Weixinpay/WxPayApi.class.php');
        require_once($BasePath.'/Weixinpay/WxPayJsApiPay.php');

        $paymentSet = self::getPaymentSetInfo();
        session('paymentinfo', $paymentSet);
        $tools = new \JsApiPay();
        $input = new \WxPayUnifiedOrder();
        $input->SetBody($parameters['body']);
        $input->SetAttach($parameters['attach']);
        $input->SetOut_trade_no($parameters['out_trade_no']);
        $input->SetTotal_fee($parameters['totalFee'] * 100); // 单位为分
        $input->SetNotify_url($parameters['notify_url']);
        $input->SetTrade_type("JSAPI");
        $input->SetOpenid($openId);

        $result = \WxPayApi::unifiedOrder($input);
        if($result['return_code'] != 'SUCCESS') throw new Exception("支付失败[{$result['return_msg']}]", 50001);
        if($result['result_code'] != 'SUCCESS') throw new Exception("支付失败({$result['err_code']})[{$result['err_code_des']}]", 50002);
        $jsApiParameters = $tools->GetJsApiParameters($result);
        return json_decode($jsApiParameters, true);
    }

    /**
     * 退款
     *
     * @param string $token
     * @param string $tradeNo
     * @param float $tradeAmount
     * @param string $refundTradeNo
     * @param float $refundTradeAmount
     * @return array
     * @throws Exception
     */
    public static function refund($tradeNo, $tradeAmount, $refundTradeNo, $refundTradeAmount,$refundDesc=''){
        $BasePath=SITE_PATH."/Application/Common";
        require_once($BasePath.'/Weixinpay/WxPayData.class.php');
        require_once($BasePath.'/Weixinpay/WxPayApi.class.php');
        require_once($BasePath.'/Weixinpay/WxPayJsApiPay.php');
        require_once($BasePath.'/Weixinpay/WxPayConfig.php');

        $paymentSet = self::getPaymentSetInfo();
        //检测证书安全性
        $cert = self::certHandler($paymentSet);
        $paymentSet['cert_path'] = $cert[0];
        $paymentSet['key_path'] = $cert[1];
        session('paymentinfo', $paymentSet);
        $input = new \WxPayRefund();
        $input->setAppId($paymentSet['wxappid']);
        $input->SetOut_trade_no($tradeNo);
        $input->SetTotal_fee($tradeAmount * 100);
        $input->SetOut_refund_no($refundTradeNo);
        $input->SetRefund_fee($refundTradeAmount * 100);
        $input->SetRefund_desc($refundDesc);
        $input->SetOp_user_id($paymentSet['wxmchid']);
        $result = \WxPayApi::refund($input);
        //检查微信返回接口是否错误
        if($result['return_code'] != 'SUCCESS') throw new Exception("退款失败[{$result['return_msg']}]", 50001);
        if($result['result_code'] != 'SUCCESS') throw new Exception("退款失败({$result['err_code']})[{$result['err_code_des']}]", 50002);
        return $result;
    }

    /**
     * 获取支付结果信息
     *
     * @return bool|mixed
     * @throws Exception
     */
    public final static function getPaymentResult(){
        $input = file_get_contents('php://input');
        if(empty($input)) throw new Exception('获取失败！', 500);
        $bPreviousValue = libxml_disable_entity_loader(true);
        $obj = simplexml_load_string($input, 'SimpleXMLElement', LIBXML_NOCDATA);
        $data = json_decode(json_encode($obj), true);
        libxml_disable_entity_loader($bPreviousValue);
//        hx_write_app_log(var_export($data, true),'wepay_log');
        if(empty($data)) throw new Exception('获取失败！', 500);

        if(!isset($data['sign']) || empty($data['sign'])){
            throw new Exception("签名错误:[sign is empty or sign doesn't exist]！", 500);
        }
        $paymentSet = self::getPaymentSetInfo();
        $paymentKey = $paymentSet['wxpaysignkey'];
        $sign = self::makeSign($data, $paymentKey);

        if($data['sign'] != $sign){
            throw new Exception("签名错误:[sign check fail]！", 500);
        }

        if($data['result_code'] != 'SUCCESS' || $data['return_code'] != 'SUCCESS'){
            throw new Exception($data['return_msg'] ? $data['return_msg'] : $data['result_msg']);
        }
        Log::record("获取微信支付回调信息结束", Log::ERR, true);
        return $data;
    }

    /**
     * 制作随机字符串
     *
     * @param int $length
     * @return string
     */
    public static function buildNonceStr($length = 32){
        $chars = "abcdefghijklmnopqrstuvwxyz0123456789";
        $str = "";
        for($i = 0; $i < $length; $i++){
            $str .= substr($chars, mt_rand(0, strlen($chars) - 1), 1);
        }

        return $str;
    }

    /**
     * 编译请求参数
     *
     * @param array $parameters
     * @return string
     */
    public static function buildParameter($parameters){
        $buff = "";
        foreach($parameters as $k => $v){
            if($k != "sign" && $v != "" && !is_array($v)){
                $buff .= $k."=".$v."&";
            }
        }
        $buff = trim($buff, "&");
        return $buff;
    }

    /**
     * 编译请求参数
     *
     * @param array $parameters
     * @return string
     * @throws Exception
     */
    public static function buildParameterToXml($parameters){
        if(!is_array($parameters) || count($parameters) <= 0){
            throw new Exception("数组数据异常！", 500);
        }

        $xml = "<xml>";
        foreach($parameters as $key => $val){
            if(is_numeric($val)){
                $xml .= "<".$key.">".$val."</".$key.">";
            }else{
                $xml .= "<".$key."><![CDATA[".$val."]]></".$key.">";
            }
        }
        $xml .= "</xml>";

        return $xml;
    }

    /**
     * 解析xml为数组
     *
     * @param string $xml
     * @return array
     * @throws Exception
     */
    public static function parseXml($xml){
        if(empty($xml)) throw new Exception("xml数据异常！", 500);

        $xml = simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA);
        $xml = json_encode($xml);

        return (array)json_decode($xml, true);
    }

    /**
     * 以post方式提交xml到对应的接口url
     * Enter description here ...
     *
     * @param string $xml 需要post的xml数据
     * @param string $url url
     * @param array $cert 是否需要证书，默认不需要
     * @param int $second url执行超时时间，默认30s
     * @return string
     * @throws Exception
     */
    public static function postXmlCurl($xml, $url, $cert = null, $second = 30){
        //初始化curl
        $ch = curl_init();
        //设置超时
        curl_setopt($ch, CURLOPT_TIMEOUT, $second);
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
        curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
        //设置header
        curl_setopt($ch, CURLOPT_HEADER, false);
        //要求结果为字符串且输出到屏幕上
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

        //设置证书
        //使用证书：cert 与 key 分别属于两个.pem文件
        if(is_array($cert)){
            curl_setopt($ch, CURLOPT_SSLCERTTYPE, 'PEM');
            curl_setopt($ch, CURLOPT_SSLCERT, $cert['SSLCERT']);
            curl_setopt($ch, CURLOPT_SSLKEYTYPE, 'PEM');
            curl_setopt($ch, CURLOPT_SSLKEY, $cert['SSLKEY']);
        }

        //post提交方式
        curl_setopt($ch, CURLOPT_POST, true);
        curl_setopt($ch, CURLOPT_POSTFIELDS, $xml);
        //运行curl
        $data = curl_exec($ch);
        //返回结果
        if($data){
            curl_close($ch);

            return $data;
        }else{
            $error = curl_errno($ch);
            curl_close($ch);
            throw new Exception("请求错误：$error");
        }
    }

    /**
     * 获取支付配置信息
     *
     * @param string $token
     * @return array
     * @throws Exception
     */
    private static function getPaymentSetInfo(){
        $app_config_info = BaseData::getAppSetting();
        $paymentInfo['wxappid'] = trim($app_config_info['wx_app_appid']);
        $paymentInfo['wxmchid'] = trim($app_config_info['wepay_shanghuhao']);
        $paymentInfo['wxappsecret'] = trim($app_config_info['wx_app_appsecret']);
        $paymentInfo['wxpaysignkey'] = trim($app_config_info['wepay_partnerkey']);
        if(empty($paymentInfo['wxappid'])) throw new Exception('请配置微信支付[appid]', 400);
        if(empty($paymentInfo['wxmchid'])) throw new Exception('请配置微信支付[商户账号]', 400);
        if(empty($paymentInfo['wxappsecret'])) throw new Exception('请配置微信支付[appsecret]', 400);
        if(empty($paymentInfo['wxpaysignkey'])) throw new Exception('请配置微信支付[支付密钥]', 400);
        return $paymentInfo;
    }

    /**
     * 证书处理
     *
     * @param array $paymentInfo
     * @return array
     * @throws Exception
     */
    private static function certHandler(){
        $app_config_info = BaseData::getAppSetting();
        $certPath = SITE_PATH.substr($app_config_info['wepay_cert_pem'],1);
        $keyPath = SITE_PATH.substr($app_config_info['wepay_key_pem'],1);
        return array($certPath, $keyPath);
    }

    /**
     * 生成签名
     *
     * @param $parameters
     * @param $key
     * @return string
     */
    private static function makeSign($parameters, $key){
        //签名步骤一：按字典序排序参数
        ksort($parameters);
        $parameterStr = self::buildParameter($parameters);
        //签名步骤二：在string后加入KEY
        $parameterStr .= "&key=".$key;
        //签名步骤三：MD5加密
        $sign = md5($parameterStr);
        //签名步骤四：所有字符转为大写
        return strtoupper($sign);
    }

    /**
     * 微信提现到银行卡
     * @param $token
     * @param $parame_data
     * @return array|string
     * @throws Exception
     */
    public function mmpaysptrans($parame_data){
        $paymentInfo = self::getPaymentSetInfo();
        $wx_pub_key = self::getWxRSA();
        $partner_trade_no = $parame_data['partner_trade_no'];
        $enc_bank_no = $parame_data['enc_bank_no'];
        $enc_true_name = $parame_data['enc_true_name'];
        $bank_code = $parame_data['bank_code'];
        $amount = $parame_data['amount'];
        $desc = $parame_data['desc'];

        if(empty($partner_trade_no)) throw new Exception('订单号不能为空！', 401);
        if(empty($enc_bank_no)) throw new Exception('银行卡号不能为空！', 401);
        if(empty($enc_true_name)) throw new Exception('收款方用户名不能为空！', 401);
        if(empty($bank_code)) throw new Exception('开户行不能为空！', 401);
        if(empty($amount)) throw new Exception('付款金额不能为空！', 401);


        //基本参数
        $parameters = array();
        $parameters['mch_id'] = $paymentInfo['wxmchid'];
        $parameters['partner_trade_no'] = $partner_trade_no;
        $parameters['nonce_str'] = self::buildNonceStr();
        $parameters['enc_bank_no'] = self::phpRsaEncrypt($wx_pub_key,$enc_bank_no);
        $parameters['enc_true_name'] = self::phpRsaEncrypt($wx_pub_key,$enc_true_name);
        $parameters['bank_code'] = $bank_code;
        $parameters['amount'] = $amount*100;
        $parameters['desc'] = $desc;

        //检测证书安全性
        $cert = self::certHandler($paymentInfo);
        $cert = array('SSLCERT' => $cert[0], 'SSLKEY' => $cert[1],);
        //参数排序
        ksort($parameters, SORT_STRING);
        //生成签名
        $parameters['sign'] = self::makeSign($parameters, $paymentInfo['wxpaysignkey']);
        //编译请求参数为xml进行请求
        $xml = self::buildParameterToXml($parameters);
        $url = 'https://api.mch.weixin.qq.com/mmpaysptrans/pay_bank';
        $result = self::postXmlCurl($xml, $url, $cert);
        $result = self::parseXml($result);
        //检查微信返回接口是否错误
        if($result['return_code'] != 'SUCCESS') throw new Exception("付款失败[{$result['return_msg']}]", 50001);
        if($result['result_code'] != 'SUCCESS') throw new Exception("付款失败({$result['err_code']})[{$result['err_code_des']}]", 50002);

        return $result;
    }

    /**
     * 获取RSA公钥
     */
    private static function getWxRSA(){
        $payment_set = M("app_pay_config");
        $payment_info = $payment_set->order("id Asc")->find();
        if($payment_info['wx_rsa_data']){
            $pub_key = $payment_info['wx_rsa_data'];
        }else{
            $paymentInfo = self::getPaymentSetInfo();
            //基本参数
            $parameters = array();
            $parameters['mch_id'] = $paymentInfo['wxmchid'];
            $parameters['nonce_str'] = self::buildNonceStr();
            //检测证书安全性
            $cert = self::certHandler($paymentInfo);
            $cert = array('SSLCERT' => $cert[0], 'SSLKEY' => $cert[1],);
            //参数排序
            ksort($parameters, SORT_STRING);
            //生成签名
            $parameters['sign'] = self::makeSign($parameters, $paymentInfo['wxpaysignkey']);
            //编译请求参数为xml进行请求
            $xml = self::buildParameterToXml($parameters);

            $url = 'https://fraud.mch.weixin.qq.com/risk/getpublickey';
            $result = self::postXmlCurl($xml, $url, $cert);
            $result = self::parseXml($result);
            //检查微信返回接口是否错误
            if($result['return_code'] != 'SUCCESS') throw new Exception("提现失败[{$result['return_msg']}]", 50001);
            if($result['result_code'] != 'SUCCESS') throw new Exception("提现失败({$result['err_code']})[{$result['err_code_des']}]", 50002);
            $pub_key = $result['pub_key'];
            $s_data['wx_rsa_data'] = $pub_key;
            $map['id'] = $payment_info['id'];
            $payment_set->where($map)->save($s_data);
        }
        return $pub_key;
    }

    /**
     * php RSA通用公钥加密
     */
    private static function phpRsaEncrypt($pub_key,$data){
        $pub_key = self::changeRsaStyle($pub_key);
        $pu_key = openssl_pkey_get_public($pub_key);
        openssl_public_encrypt($data, $encrypted, $pu_key,OPENSSL_PKCS1_OAEP_PADDING);
        $encrypted = base64_encode($encrypted);
        return $encrypted;
    }

    private static function changeRsaStyle($public_key){
        $start_key = str_replace('-----BEGIN RSA PUBLIC KEY-----', '', $public_key);
        $start_key = trim(str_replace('-----END RSA PUBLIC KEY-----', '', $start_key));
        $key = 'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A' . str_replace("\n", '', $start_key);
        $key = "-----BEGIN PUBLIC KEY-----\n" . wordwrap($key, 64, "\n", true) . "\n-----END PUBLIC KEY-----";
        return $key;
    }
}